\chapter{具体的实验和算法}
\section{实验流程}
整个实验流程可以分为三个部分：

一是导入高模和低模，通过相机垂直投影分别获取模型的深度值并存储，获取完毕后对两份深度值做减法，计算得出深度差值,并将其以图片的形式导出，作为高度贴图。

二是对低模进行曲面细分，在程序中调用相应的OpenGL函数，并在OpenGL渲染管线中编写曲面细分着色器，处理从模型输入的顶点数据，增加模型原本的顶点数和面数，从而在一定程度上提高模型的精度

三是将低模，低模对应的纹理贴图以及生成的高度贴图导入UE4，将这戏作为组成部分连入材质蓝图中，生成包含纹理信息和顶点位移信息的材质，并添加到低模上。

\begin{figure}[ht]
    \centering
    \includegraphics[height=10cm]{images/系统流程.jpg}
    \caption{实验流程}
    \label{fig:my_label}
\end{figure}

\section{计算深度差值}
\subsection{模型导入}
本文使用Assimp(Open Asset Import Library)开源模型导入库，导入所需的高模和低模。Assimp库是当今非常流行的一种模型导入库，支持大部分主流的3D模型格式，并会将导入后的模型数据加载到Assimp的通用数据结构中。模型会被加载到一个Sence对象中，其包含mRoot场景根节点的引用，以及指向网格和材质的索引。开发人员通过调用Assimp提供的接口，传入对应的索引，便可得到存储在各个结点中网格数据或材质数据。

\begin{figure}[ht]
    \centering
    \includegraphics[height=8cm]{images/Assimp.png}
    \caption{Assimp}
    \label{fig:my_label}
\end{figure}

本文将模型的每个网格单独作为一个VAO在OpenGL中绑定，将每个网格顶点的Position，Normal，TexCoord按规律存储到数组中，作为顶点数据统一发送。处理完毕后的模型被抽象为了Model类型对象，当需要绘制模型时，直接调用Model对象的Draw()函数即可。

\subsection{获取深度值}

首先创建两个帧缓冲LowModelFrameBuffer和HighModelFrameBuffer(以下分别简称为LFB和HFB，帧缓冲简称为FB)，分别用于存储低模和高模的各部分的深度值，再创建一个DifferFrameBuffer(以下简称DFB)，用于存储高模和低模的深度差值。对于每个FB，创建一个纹理对象，以颜色缓冲的形式绑定，设置数据格式为RGB32F(即包含R, G, B三个分量，每个分量皆为32位的浮点数)，同时创建一个渲染缓冲对象，以深度缓冲的形式绑定，数据格式为32位的浮点数。当需要获取低模的深度值时，绑定LFB，渲染低模并输出深度值到LFB的颜色缓冲中；当血药获取高模的深度值时，绑定HFB，渲染高模并输出深度值到HFB的颜色缓冲中。

原本在顶点着色器中会执行下面的矩阵乘法：
\begin{equation*}
    {Projection}\times{View}\times{Model}\times{Position}
\end{equation*}

为了将高模和低模都放入观察空间中，本文在顶点着色器中新设立一个4分量输出值FragPos，记录在观察空间下的顶点坐标并输出。此时顶点着色器中的矩阵计算为：
\begin{equation*}
    FragPos={View}\times{Model}\times{Position}
\end{equation*}

计算出的FragPos会经过光栅器，进行片段插值，然后输入片段着色器，这样就可以得到每个片段的坐标值在观察空间下的大小。将该值以4分量灰度的形式输出到绑定帧缓冲的颜色缓冲中:
\begin{equation*}
    vector4(vector3(FragPos.z), 1.0)
\end{equation*}
本文所处理的模型目标为平面地图模型。不同于其他复杂模型，平面地图模型的高模和低模的差异，往往表现在模型顶点的z值上。因此，在相机投影的方式上，可以选择忽略高模和低模顶点坐标在其他方向向量上的差异，无需计算模型表面法线的切线等数据，直接将相机放置在地图正上方(即相机位置朝z轴方向偏移)即可。然后执行上述步骤，即可得到模型的深度值。

\subsection{计算并输出深度差值}

分别渲染一遍低模和高模后，得到低模和高模的深度值，并存储在LFB和HFB的颜色缓冲中。由于LFB和HFb的颜色缓冲都是纹理对象，因此可以按照纹理对象的方式使用高模和低模对应的颜色缓冲。

在OpenGL程序中，激活TEXTURE0和TEXTURE1，分别绑定两个颜色缓冲对应的纹理对象，将其传入片段着色器中。调用texture()纹理采样函数，传入当前片段对应的纹理坐标，获取同一屏幕坐标下的深度值，然后做减法获取深度差值Difference。将该深度差值以4分量灰度值的形式输出到DFB中：
\begin{equation*}
    vector4(vector3(Difference), 1.0)
\end{equation*}
最后将DFB中的数据导出生成JPG格式图像文件，即得到所需的高度贴图

\section{低模曲面细分}
\subsection{设置曲面细分着色器}
在OpenGL中，开发人员想要启用曲面细分，必须要自定义编写曲面细分着色器，并在程序中调用相应的OpenGL函数。当指定OpenGL绘制模型时的图元类型时，OpenGL会隐式知道一个图元需要多少个顶点，例如Triangle对应3个顶点。而当我们启用曲面细分时，会在绘制时将图元类型设置为Patch,此时需要调用glPatchParameteri()函数，指定一个Patch对应多少个顶点。根据后续算法的需要，这里设定glPatchParameteri的参数为3，即一个Patch对应3个顶点。

在所有可编写的着色器中，创建Point类型结构体，包含Position，Normal，TexCoord等变量，用于表达一个完整的顶点属性。

在顶点着色器中，将传入渲染线管的数据组合成Point结构并输出。

在曲面细分控制着色器中，创建Uniform变量，TessLevelInnerInput和TessLevelOuterInput，获取从程序传来的输入，从而通过键入的方式控制曲面细分的程度。因为细分空间设定为三角形，因此对TessLevelInner数组和TessLevelOuter数组作如下设置：
\begin{equation*}
    TessLevelInner[0]=TessLevelInnerInput
\end{equation*}
\begin{equation*}
    TessLevelOuter[0]=TessLevelOuterInput
\end{equation*}
\begin{equation*}
    TessLevelOuter[1]=TessLevelOuterInput
\end{equation*}
\begin{equation*}
    TessLevelOuter[2]=TessLevelOuterInput
\end{equation*}
其中，第一句赋值表示该三角形细分空间内部划分区域的个数，后三句赋值表示该三角形细分空间的三条边的划分段数。同时，在控制着色器中创建Point型变量，接受从顶点着色器传入的Point型数据并输出。

在曲面细分计算着色器中，创建Point型变量接受经过曲面细分引擎细分后的控制点数据，以及三角形细分空间的质心，根据这些数，使用PN三角形算法计算新顶点的Position，Normal，TexCoord等数据并输出。

在片段着色器中创建Point型变量，接受来自曲面细分计算着色器的输出，根据新生成的纹理坐标TexCoord对原纹理进行采样。

\subsection{PN三角形}

曲面细分计算着色器采用的算法，直接关系着最后模型曲面细分的效果。本文决定采用较为流行PN三角形算法\cite{陈驰2015pn}，该算法计算生成的新的顶点和面可以更加有效地丰富原模型的细节轮廓。PN三角形是一种特殊的贝塞尔曲线，其思路是根据最开始的3个控制点(即Patch的3个顶点)信息，计算出新的控制点的坐标和法向量，最后根据所有的控制点信息和质心坐标，计算出新生成顶点的坐标和法向量。PN三角形一共包含10个控制点坐标，6个控制点法线。

设置Patch的三个顶点坐标为${P_1}$，${P_2}$，${P_3}$，其对应的顶点法向量为${N_1}$，${N_2}$，${N_3}$，对应的顶点纹理坐标为${T_1}$，${T_2}$，${T_3}$。令计算新顶点所需的控制点坐标为${b_{ijk}}$，其中i+j+k=3，P为${b_{ijk}}$所对应的在三角形上的插入点，其中(i，j，k)分别代表计算该插入点时，取${P_1}$，${P_2}$，${P_3}$坐标的权重，其计算公式为:
\begin{equation*}
    P={({i}\times{P_1}+{j}\times{P_2}+{k}\times{P_3})} / 3
\end{equation*}
因此，Patch的3个顶点对应的顶点坐标分别为${b_{300}}$，${b_{030}}$，${b_{003}}$b。计算时取的权重越大，代表该插入点离对应的Patch顶点越近。以距离P点最近的顶点所在的平面为投影平面，该顶点的法向量为平面对应的法向量，则P点在该平面上的投影点即为所求的新增控制点。定义新变量wij，其计算公式为：
\begin{equation*}
    w_{ij}={({P_j}-{P_i})}\times{N_i}
\end{equation*}
则可以得到除b111以外的控制点的计算公式为
\begin{equation*}
    b_{ijk}=P-(w_{ij}\times{N_i}) / 3
\end{equation*}
从而计算出三角形边上的6个控制点坐标。
在令
\begin{equation*}
    E=(b_{210}+b_{120}+b_{021}+b_{012}+b_{102}+b_{201})/6
\end{equation*}
\begin{equation*}
    V=({P_1}+{P_2}+{P_3})/3
\end{equation*}
得到最后一个控制点坐标${b_{111}}$的计算公式：
\begin{equation*}
    b_{111}=E+(E-V)/2
\end{equation*}
从而得到所有控制点坐标。

\begin{figure}[ht]
    \centering
    \includegraphics[height=5cm]{images/控制点坐标.png}
    \caption{控制点坐标示例}
    \label{fig:my_label}
\end{figure}

令计算新顶点法线所需的控制点法向量为${n_{ijk}}$，其中i+j+k=2。${N_1}$，${N_2}$，${N_3}$分别对应${n_{200}}$，${n_{020}}$，${n_{002}}$，根据这三个控制点法向量可计算得出剩下的控制点法向量。这里不能简单的进行线性插值，因为极有可能存在一条边两端的法向量方向相同，但中间部分的法向量方向和两端不同的情况，简单的进行线性插值会导致计算出的中间部分控制点法向量方向误差较大。设${n_{ijk}}$对应的线性插值结果的法向量方向为
\begin{equation*}
    (i\times{N_1}+j\times{N2}+k\times{N3})/2
\end{equation*}

其在对应三角形边上的投影为N，则可得该法向量在垂直于对应边上的投影向量${n_{ijk}}$为：
\begin{equation*}
    n_{ijk}=(i\times{N_1}+j\times{N_2}+k\times{N_3})/2-N
\end{equation*}

为实际计算出${n_{jik}}$，设置变量${v_{nm}}$，其计算公式为：
\begin{equation*}
    v_{nm}=2\times(P_m-P_n)(N_n+N_m)/(P_m-P_n)(P_m-P_n)
\end{equation*}

其中(n, m)分别为(i, j, k)中数值不为0的分量所在的位次。如当(i, j, k)的值为(1, 1, 0)时，(n, m)的值为(1, 2)。则可得${n_{ijk}}$的计算公式为：
\begin{equation*}
    n_{ijk}=\frac{(i\times{N_1}+j\times{N_2}+k\times{N_3}-v_{nm}(P_m-P_n))}{||i\times{N_1}+j\times{N_2}+k\times{N_3}-v_{nm}(P_m-P_n)||}
\end{equation*}

从而得到所有控制点法向量。

\begin{figure}[ht]
    \centering
    \includegraphics[height=5cm]{images/控制点法向量.png}
    \caption{控制点法向量示例}
    \label{fig:my_label}
\end{figure}

设对应三角形的质心坐标为(u, v, w)，并根据顶点坐标和定点法向量的计算公式：
\begin{equation*}
    b=\sum{}{i+j+k=3}b_{ijk}\frac{3!}{i!j!k!}{u^i}{v^j}{w^k}
\end{equation*}
\begin{equation*}
    n=\sum{}{i+j+k=2}n_{ijk}{u^i}{v^j}{w^k}
\end{equation*}

得到新生成顶点的坐标和法向量。

最后根据质心坐标对纹理坐标直接进行差值，公式如下：
\begin{equation*}
    t=T_1\times{u}+T_2\times{v}+T_3\times{w}
\end{equation*}
将得到的顶点坐标b，顶点法向量n以及顶点纹理坐标t组合成Point类型数据并输出。

\section{作用到低模}
Unreal Engine 4，即虚幻引擎，是业界中主流的游戏开发引擎和实时3D创作平台，拥有大量图形学相关功能和接口，能够为开发人员提供一定程度上的便利。UE4引擎以C++作为主要编程语言，但同时该引擎也提供了基于C++的可视化编程语言——蓝图，开发人员对蓝图进行连线、设置参数等操作，能很快实现C++程序所实现的大部分功能。蓝图作为可视化的编程语言，其本身也提供了各种参数接口，开发人员将参数与接口相连，便能调整对应程序的执行方式和执行结果。
\begin{figure}[ht]
    \centering
    \includegraphics[height=9cm]{images/蓝图示例.png}
    \caption{蓝图示例}
    \label{fig:my_label}
\end{figure}

新建材质文件，并将其以蓝图的形式打开。将地图模型对应的纹理贴图，作为“基础颜色”的参数；将高度贴图和偏移乘数相乘，作为“世界场景位置偏移”的参数；设置常量曲面细分乘数，作为“曲面细分乘数”的参数。连接后的蓝图如下所示：
\begin{figure}[ht]
    \centering
    \includegraphics[height=7cm]{images/蓝图.png}
    \caption{蓝图}
    \label{fig:my_label}
\end{figure}

至此，模型材质制作完毕。其包含低模对应的纹理贴图信息，高模映射到低模的高度贴图信息以及曲面细分设置参数信息。最后，在UE4中将该材质添加到低模对应的组件位置，并和原低模和高模进行比较，记录比较结果，即可完成整个实验流程。